﻿
namespace Patterns4Net.PatternEnforcer.Core.Patterns
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics.Contracts;
    using System.Linq;
    using Mono.Cecil;
    using Mono.Cecil.Cil;
    using Patterns4Net.Core.CecilExtensions;
    using Patterns4Net.Core.CecilExtensions.CILPatternsMatching;
    using Patterns4Net.Core.Patterns;

    [PatternCheckerExport(typeof(Singleton))]
    public class SingletonChecker : PatternCheckerBase<Singleton>
    {
        protected override CheckerResult Check(Singleton pattern)
        {
            var result = new CheckerResultBuilder();
            var type = pattern.TargetType;
            bool isValid = true;

            // private constructor
            result.AddCheckedRule();
            var ctors = type.Methods.Where(x => x.IsConstructor);
            if (ctors.Any(x => x.IsPublic))
            {
                isValid = false;
                result.Add(ErrorMessages.SingletonMustHaveAPrivateCtor, type.Name);
            }

            // Next two check are here just to produce more detailed error, but
            // these checks are logically part of the following one, so we does not increment checkedRulesCout
            if (ContainsOneFieldOfItsType(type) == false)
            {
                if (isValid == false)
                {
                    // So we don't have more errors than checked rules.
                    result.AddCheckedRule();
                }
                result.Add(ErrorMessages.SingletonClassDoesNotContainStaticInstanceField, type.Name);
                isValid = false;
            }

            if (ContainsMoreThanOneCallToConstructor(type))
            {
                if (isValid == false)
                {
                    // So we don't have more errors than checked rules.
                    result.AddCheckedRule();
                }

                isValid = false;
                result.Add(ErrorMessages.SingletonClassDoesNotContainStaticInstanceField, type.Name);                
            }

            result.AddCheckedRule();
            if (isValid)
            {
                if (IsSingletonInstanceInitializedInStaticCtor(type) == false &&
                    IsSingletonInstanceInitializedInNestedClass(type) == false &&
                    IsSingletonInstanceInitializedInGetInstanceMethod(type) == false)
                {
                    result.Add(ErrorMessages.SingletonImplementationIsNotValid, type.Name);
                }
            }

            return result.Build();
        }

        private static bool IsSingletonInstanceInitializedInStaticCtor(TypeDefinition singletonType)
        {
            return IsSingletonInstanceInitializedInStaticCtor(singletonType, singletonType);
        }

        private static bool IsSingletonInstanceInitializedInStaticCtor(TypeDefinition typeOrNestedType, TypeDefinition singletonType)
        {
            var method = GetAllMethodsThatCallConstructor(singletonType).SingleOrDefault();
            if (method == null || 
                method.IsConstructor == false || 
                method.IsStatic == false)
            {
                return false;
            }

            for (int i = 0; i < method.Body.Instructions.Count; i++)
            {
                if (IsconstructorCall(method.Body.Instructions[i], singletonType))
                {
                    // after this ctor call there should be stsfld (store static field) instruction
                    var instruction = method.Body.Instructions[i + 1];
                    if (instruction.OpCode != OpCodes.Stsfld)
                    {
                        return false;
                    }

                    Contract.Assume(instruction.Operand is FieldReference, "Stsfld should has operand of type FieldReference.");
                    var field = ((FieldReference)instruction.Operand).Resolve();
                    if (field == null)
                    {
                        // If the field cannot be resolved, then it is not definately our field.
                        return false;
                    }

                    // uniqueness of the static field of this type is covered in previous code.
                    return field.IsStatic && field.FieldType.IsEqual(singletonType) && field.DeclaringType.IsEqual(typeOrNestedType);
                }
            }

            Contract.Assert(
                false, 
                "When we are in IsSingletonInstanceInitializedInStaticCtor the type should have at " + 
                " least one method with at least one ctor call");
            return false;
        }

        private static bool IsSingletonInstanceInitializedInNestedClass(TypeDefinition type)
        {
            if (type.HasNestedTypes == false)
            {
                return false;
            }

            var initializingNestedTypes =
                type.NestedTypes.Where(nested => IsSingletonInstanceInitializedInStaticCtor(nested, type));
            return initializingNestedTypes.Count() == 1;
        }

        // HACK: to be removed.
        private static TypeDefinition mytype;

        private static bool IsMyField(object operand)
        {
            return operand is FieldReference &&
                                ((FieldReference)operand).DeclaringType.IsEqual(mytype) &&
                                ((FieldReference)operand).FieldType.IsEqual(mytype);            
        }

        private static bool IsSingletonInstanceInitializedInGetInstanceMethod(TypeDefinition type)
        {
            /*
             * Here we will check presence of this "code pattern":
             *  // whatever
             *  if (instance == null)
             *  {
             *      // whatever
             *      instance = new MyType();
             *      // whatever
             *  }
             *  
             *  // whatever
             *  return instance;
             *  
             *  This code pattern compiled by csc into two differenc forms of CIL according to whether optimization was enabled.
             *  So we will check both of these forms here:
             */
            mytype = type;
            Func<object, bool> isTheSingletonField = 
                operand => operand is FieldDefinition && 
                    ((FieldDefinition)operand).DeclaringType.IsEqual(type) && 
                    ((FieldDefinition)operand).FieldType.IsEqual(type);

            var newobjMatcher = new CILPatternsMatcher(
                new OpCodeInstructionMatcher(OpCodes.Newobj, ctor => ((MethodReference)ctor).DeclaringType.IsEqual(type)),
                new OpCodeInstructionMatcher(OpCodes.Stsfld, isTheSingletonField));

            var releaseReturnMatcher = new CILPatternsMatcher(
                new OpCodeInstructionMatcher(OpCodes.Ldsfld, isTheSingletonField),
                new OpCodeInstructionMatcher(OpCodes.Ret));

            var debugReturnMatcher = new CILPatternsMatcher(
                new OpCodeInstructionMatcher(OpCodes.Ldsfld, isTheSingletonField),
                new PredicateInstructionMatcher(i => i.NormalizedOpCode() == OpCodes.Stloc),
                new PredicateInstructionMatcher(i => i.NormalizedOpCode() == OpCodes.Ldloc),
                new OpCodeInstructionMatcher(OpCodes.Ret));

            var debugIfMatcher = new CILPatternsMatcher(
                new OpCodeInstructionMatcher(OpCodes.Ldsfld, IsMyField),
                new OpCodeInstructionMatcher(OpCodes.Ldnull),
                new OpCodeInstructionMatcher(OpCodes.Ceq),
                new OpCodeInstructionMatcher(OpCodes.Ldc_I4_0),
                new OpCodeInstructionMatcher(OpCodes.Ceq),
                new PredicateInstructionMatcher(i => i.NormalizedOpCode() == OpCodes.Stloc),
                new PredicateInstructionMatcher(i => i.NormalizedOpCode() == OpCodes.Ldloc),
                new OpCodeInstructionMatcher(OpCodes.Brtrue));

            var releaseIfMatcher = new CILPatternsMatcher(
                new OpCodeInstructionMatcher(OpCodes.Ldsfld, isTheSingletonField),
                new OpCodeInstructionMatcher(OpCodes.Brtrue));

            var debugFormMatcher = new CILPatternsMatcher(
                new FreeFormInstructionMatcher(debugIfMatcher),
                new FreeFormInstructionMatcher(newobjMatcher),
                new FreeFormInstructionMatcher(debugReturnMatcher));

            var releaseFormMatcher = new CILPatternsMatcher(
                new FreeFormInstructionMatcher(releaseIfMatcher),
                new FreeFormInstructionMatcher(newobjMatcher),
                new FreeFormInstructionMatcher(releaseReturnMatcher));

            foreach (var method in type.Methods.Where(m => m.ReturnType.IsEqual(type) && m.IsStatic && m.HasBody))
            {
                var firstInstruction = method.Body.Instructions.First();
                if (debugFormMatcher.Matches(firstInstruction) ||
                    releaseFormMatcher.Matches(firstInstruction))
                {
                    return true;
                }
            }

            return false;
        }

        private static bool IsconstructorCall(Instruction instruction, TypeDefinition type)
        {
            if (instruction.OpCode == OpCodes.Newobj)
            {
                Contract.Assume(instruction.Operand is MethodReference, "Newobj instruction should has operand of type MethodReference.");
                var called = ((MethodReference)instruction.Operand).Resolve();
                return called != null && called.DeclaringType.IsEqual(type) && called.IsConstructor;
            }

            return false;            
        }

        private static IEnumerable<MethodDefinition> GetAllMethodsThatCallConstructor(TypeDefinition type)
        {
            IEnumerable<TypeDefinition> nestedTypes = type.NestedTypes;
            if (type.HasNestedTypes == false)
            {
                nestedTypes = Enumerable.Empty<TypeDefinition>();
            }

            return type.Methods.Concat(nestedTypes.SelectMany(x => x.Methods))
                .Where(m => m.Body.Instructions.Any(x => IsconstructorCall(x, type)));
        }

        private static bool ContainsMoreThanOneCallToConstructor(TypeDefinition type)
        {
            return
                GetAllMethodsThatCallConstructor(type).Sum(
                    m => m.Body.Instructions.Count(i => IsconstructorCall(i, type))) > 1;
        }

        private static bool ContainsOneFieldOfItsType(TypeDefinition type)
        {
            IEnumerable<TypeDefinition> types = new[] { type };
            if (type.HasNestedTypes)
            {
                types = types.Concat(type.NestedTypes);
            }

            var fields = types.Where(t => t.HasFields).SelectMany(t => t.Fields);
            return fields.Count(field => field.FieldType.IsEqual(type)) == 1;
        }
    }
}
